/*
 * XXX
 */

package edu.uhcl.csci4838.group3.calc;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.content.pm.ComponentInfo;
import android.util.Log;
import edu.uhcl.csci4838.group3.dto.Category;
import edu.uhcl.csci4838.group3.dto.Course;
import edu.uhcl.csci4838.group3.dto.Task;

/**
 * This class handles all the calculation for figuring out grades. It is assumed
 * that everything is based of a 100% scale. If the the grade adds up to over
 * 100% then it is assumed that the amount above is bonus.
 * 
 * @author Tyler Hays
 */
public class GradeCal {
	
	private static final String TAG = ComponentInfo.class.getCanonicalName();
	
	/**
	 * Calculates the grade for the respective category and returns it
	 * 
	 * @param categoryToGrade
	 *            category needing grading
	 * @return The Categories numerical grade
	 * @throws CategoryException
	 *             task override weight total exceeds category weight
	 */
	public static Double categoryGrade(final Category categoryToGrade) throws CategoryException {
		Double categoryWeight;
		Double gradeReturn = null;
		
		categoryWeight = categoryToGrade.getWeight();
		
		if (categoryWeight == null) {
			gradeReturn = GradeCal.categoryWithOutWeight(categoryToGrade);
		} else {
			gradeReturn = GradeCal.categoryWithWeight(categoryToGrade,
					categoryWeight.doubleValue() / 100);
		}
		
		return gradeReturn;
	}
	
	/**
	 * Returns grade needed on final for a certain grade.
	 * 
	 * @param course
	 *            Course that needs final grade
	 * @param requiredGrade
	 *            Minimum required grade wanted
	 * @param taskNeedingGrade
	 *            Task that break down of required grade that is needed
	 * @return required grade to earn on final.
	 * @throws MissingInfoException
	 *             if requiredGrade is null
	 * @throws CategoryException
	 *             task override weight total exceeds category weight
	 */
	public static Double gradeNeededOnFinal(final Course course,
			final Double requiredGrade,
			final Task taskNeedingGrade) throws MissingInfoException,
			CategoryException {
		
		double requiredGradeOnFinal;
		double pointsEarned;
		double finalWeight;
		
		if (requiredGrade == null) {
			throw new MissingInfoException("Needs required grade"); //$NON-NLS-1$
		}
		
		finalWeight = GradeCal.getTaskWeight(taskNeedingGrade);
		pointsEarned = GradeCal.totalPointsEarned(course);
		requiredGradeOnFinal = (requiredGrade.doubleValue() - pointsEarned)
				/ finalWeight;
		return Double.valueOf(requiredGradeOnFinal);
	}
	
	/**
	 * Calc every possible grade point.
	 * 
	 * @param course
	 *            courses needing to be graded
	 * @param taskNeedingGrade
	 *            Task that break down of required grade that is needed
	 * @return final grade needed for entire list
	 * @throws MissingInfoException
	 *             some required info is missing
	 * @throws CategoryException
	 *             task override weight total exceeds category weight
	 */
	public static Map<String, Double> gradeNeededOnFinal(final Course course,
			final Task taskNeedingGrade) throws MissingInfoException,
			CategoryException {
		final Map<String, Double> gradeDistubution = course.getGradingScale()
				.getScale();
		final Map<String, Double> requiredFinalGrade = new HashMap<String, Double>(gradeDistubution.size());
		double pointsEarned;
		double gradeNeededValue;
		double requiredGrade;
		double finalWeight;
		
		finalWeight = GradeCal.getTaskWeight(taskNeedingGrade);
		pointsEarned = GradeCal.totalPointsEarned(course);
		/*
		 * loops threw and fills in return Map based on the one it receives.
		 * 	The key value will be the same as the values receive
		 */
		for (final Map.Entry<String, Double> entry : gradeDistubution.entrySet()) {
			if (entry.getValue() == null) {
				requiredFinalGrade.put(entry.getKey(),
						null);
			} else {
				
				requiredGrade = entry.getValue()
						.doubleValue();
				gradeNeededValue = (requiredGrade - pointsEarned)
						/ finalWeight;
				requiredFinalGrade.put(entry.getKey(),
						Double.valueOf(gradeNeededValue));
			}
			
		}
		
		return requiredFinalGrade;
	}
	
	/**
	 * Returns running average for class
	 * 
	 * @param course
	 *            course requiring grade
	 * @return current average in class
	 * @throws CategoryException
	 *             task override weight total exceeds category weight
	 */
	public static Double runningCourseAverage(final Course course) throws CategoryException {
		Double courseGrade = null;
		double adjustedWeight = 1;
		double categoryWeight;
		double grade = 0;
		boolean isGradeNull = true;
		Double tempGradeStorage;
		
		/*
		 * loops threw all the categories in a course calculating out the points
		 * 	and then adjusting the weights as needed for each one
		 */
		for (final Category category : course.getCategories()) {
			if (category.getWeight() == null) {
				
				tempGradeStorage = GradeCal.categoryWithOutWeight(category);
				
			} else {
				// Log.d("category.getWeight()",
				// ""
				// + category.getWeight());
				tempGradeStorage = GradeCal.categoryWithWeight(category,
						category.getWeight()
								.doubleValue() / 100);
			}
			/*
			 * if statements below handles adding up the grades
			 * 	and adjusting the weight for the overall part of 
			 * 	the grade that has been earned.			 * 	
			 */
			if (tempGradeStorage != null) {
				isGradeNull = false;
				if (category.getWeight() == null) {
					categoryWeight = GradeCal.calculateCategoryWeight(category);
				} else {
					categoryWeight = category.getWeight()
							.doubleValue() / 100;
				}
				grade += tempGradeStorage.doubleValue();
				adjustedWeight -= categoryWeight;
			}
		}
		
		if (!isGradeNull) {
			if (adjustedWeight <= 0) {
				courseGrade = Double.valueOf(grade);
			} else {
				courseGrade = Double.valueOf(grade
						/ (1 - adjustedWeight));
			}
		}
		return courseGrade;
	}
	
	/**
	 * returns category with out a weight to figure out its current overall
	 * weight
	 * 
	 * @param category
	 *            category without weight needing to calculate its weight
	 * @return category weight
	 */
	private static double calculateCategoryWeight(final Category category) {
		double categoryWeight = 0;
		
		for (final Task task : category.getTasks()) {
			if ((task.getReceivedGrade() == null)
					|| (task.getMaxGrade() == null)
					|| (task.getWeightOverride() == null)) {
				continue;
			}
			categoryWeight += task.getWeightOverride()
					.doubleValue() / 100;
		}
		return categoryWeight;
	}
	
	/**
	 * Calculates grade for category
	 * 
	 * @param categoryToGrade
	 *            category needing to be graded
	 * @return grade for category
	 */
	private static Double categoryWithOutWeight(final Category categoryToGrade) {
		final List<Task> taskList = new ArrayList<Task>();
		double totalWieght = 1;
		Double returnGrade = null;
		boolean isTotalNull = true;
		
		for (final Task task : categoryToGrade.getTasks()) {
			if ((task.getReceivedGrade() == null)
					|| (task.getMaxGrade() == null)
					|| (task.getWeightOverride() == null)) {
				continue;
			}
			isTotalNull = false;
			taskList.add(task);
			totalWieght -= task.getWeightOverride()
					.doubleValue() / 100;
		}
		
		if (!isTotalNull) {
			if (totalWieght <= 0) {
				returnGrade = Double.valueOf(GradeCal.taskGradeWeightOverride(taskList));
			} else {
				returnGrade = Double.valueOf(GradeCal.taskGradeWeightOverride(taskList)
						/ (1 - totalWieght));
			}
		}
		
		return returnGrade;
		
	}
	
	/**
	 * handles returning grade 0-100 format for a category that contains an
	 * overall weight
	 * 
	 * @param categoryToGrade
	 *            category to be graded
	 * @param categoryWeight
	 *            the weight for the category
	 * @return grade for the category in 0-100 format.
	 * @throws CategoryException
	 *             task override weight total exceeds category weight
	 */
	private static Double categoryWithWeight(final Category categoryToGrade,
			final double categoryWeight) throws CategoryException {
		
		final List<Task> taskList = categoryToGrade.getTasks();
		final List<Task> weightOverrideTask = new ArrayList<Task>();
		final List<Task> standardTask = new ArrayList<Task>();
		double adjustedWeight = categoryWeight;
		Double returnGrade = null;
		double totalGrade = 0;
		boolean isStandardNull = true;
		boolean isWeightOverrideNull = true;
		
		/*
		 *  Organize the task in those that are graded and either have override
		 *		or no override weights
		 */
		for (final Task task : taskList) {
			if ((task.getReceivedGrade() == null)
					|| (task.getMaxGrade() == null)) {
				continue;
			}
			if (task.getWeightOverride() != null) {
				weightOverrideTask.add(task);
				adjustedWeight -= task.getWeightOverride()
						.doubleValue() / 100;
				isWeightOverrideNull = false;
			} else {
				standardTask.add(task);
				isStandardNull = false;
			}
		}
		if (adjustedWeight < 0) {
			throw new CategoryException("The wieght overrides exceed category weight"); //$NON-NLS-1$
		}
		
		if (!isWeightOverrideNull) {
			totalGrade += GradeCal.taskGradeWeightOverride(weightOverrideTask);
		}
		if (!isStandardNull) {
			totalGrade += GradeCal.taskListGradewithCategoryWeight(standardTask)
					* adjustedWeight;
			
		}
		
		if (!isStandardNull
				|| !isWeightOverrideNull) {
			returnGrade = Double.valueOf(totalGrade);
		}
		
		return returnGrade;
	}
	
	/**
	 * Returns the grade for the category in weight adjusted mode
	 * 
	 * @param category
	 *            category needing to graded for calculate what is needed on
	 *            final
	 * @param categoryWeight
	 *            weight for category
	 * @return grade for category adjusted for weight
	 * @throws CategoryException
	 *             task override weight total exceeds category weight
	 */
	private static double finalGradeCategoryWeight(final Category category,
			final double categoryWeight) throws CategoryException {
		double gradeReturn = 0;
		double adjustedWieght = categoryWeight;
		final List<Task> weightOverrideTaskList = new ArrayList<Task>();
		final List<Task> standardTaskList = new ArrayList<Task>();
		boolean isWeightOverrideNull = true;
		boolean isStandardTaskNull = true;
		
		/*
		 * Separate the tasklist for the category into those with 
		 * 		Weight overrides and those with out.  Also adjust the 
		 * 		category weight to account for it.
		 */
		for (final Task task : category.getTasks()) {
			if (task.getWeightOverride() != null) {
				adjustedWieght -= task.getWeightOverride()
						.doubleValue() / 100;
				if ((task.getMaxGrade() == null)
						|| (task.getReceivedGrade() == null)) {
					continue;
				}
				weightOverrideTaskList.add(task);
				isWeightOverrideNull = false;
			}
			if (task.getMaxGrade() == null) {
				continue;
			}
			standardTaskList.add(task);
			isStandardTaskNull = false;
		}
		/*
		 * Next two if statements add to grade return return entire category with a weight 
		 * 		have it adjusted with both the items that add to overall grade weight and
		 */
		if (adjustedWieght < 0) {
			throw new CategoryException("task Wieght override exceeds weight of category."); //$NON-NLS-1$
		}
		if (!isWeightOverrideNull) {
			gradeReturn += GradeCal.taskGradeWeightOverride(weightOverrideTaskList);
		}
		if (!isStandardTaskNull) {
			gradeReturn += GradeCal.finalGradeTaskCategoryWeight(standardTaskList)
					* adjustedWieght;
		}
		
		return gradeReturn;
	}
	
	/**
	 * calculates the grade for task that just use category weight.
	 * 
	 * @param taskList
	 *            list of task that do not contain a weight override and for
	 *            Final
	 * @return grade for task
	 */
	private static double finalGradeTaskCategoryWeight(final List<Task> taskList) {
		double gradeReturn = 0;
		double maxPoints = 0;
		double earnedPoints = 0;
		for (final Task task : taskList) {
			if (task.getReceivedGrade() != null) {
				earnedPoints += task.getReceivedGrade()
						.doubleValue();
			}
			maxPoints += task.getMaxGrade()
					.doubleValue();
		}
		gradeReturn = (earnedPoints / maxPoints) * 100;
		return gradeReturn;
	}
	
	/**
	 * finalGradewithOutCategoryWeight
	 * 
	 * @param category
	 *            category needing to be graded
	 * @return grade for category withoutWeight for Final
	 */
	private static double finalGradeWithoutCategoryWeight(final Category category) {
		final List<Task> taskList = new ArrayList<Task>();
		double gradeReturn = 0;
		final boolean isNull = true;
		for (final Task task : category.getTasks()) {
			if ((task.getReceivedGrade() == null)
					|| (task.getMaxGrade() == null)
					|| (task.getWeightOverride() == null)) {
				continue;
			}
			taskList.add(task);
		}
		if (!isNull) {
			gradeReturn = GradeCal.taskGradeWeightOverride(taskList);
		}
		
		return gradeReturn;
	}
	
	/**
	 * returns the weight of a task
	 * 
	 * @param task
	 *            task needing to get its weight
	 * @return weight of task
	 * @throws MissingInfoException
	 *             required info is missing
	 */
	private static double getTaskWeight(final Task task) throws MissingInfoException {
		
		if (task.getWeightOverride() != null) {
			return task.getWeightOverride()
					.doubleValue() / 100;
		}
		return GradeCal.taskOverallWeightWithNoOverride(task);
		
	}
	
	/**
	 * Address dealing with a list of task that contain weight overrides
	 * 
	 * @param taskList
	 *            list of task with Weight Overrides
	 * @return grade in raw points form
	 */
	private static double taskGradeWeightOverride(final List<Task> taskList) {
		double receiveGrade;
		double maxGrade;
		double taskWeight;
		double totalGrade = 0;
		for (final Task task : taskList) {
			receiveGrade = task.getReceivedGrade()
					.doubleValue();
			maxGrade = task.getMaxGrade()
					.doubleValue();
			taskWeight = task.getWeightOverride()
					.doubleValue() / 100;
			totalGrade += ((receiveGrade / maxGrade)
					* taskWeight * 100);
		}
		
		return totalGrade;
	} // end finalGradeCategoryWeight
	
	/**
	 * Calculates the values of grade in 0-100 format of task with no wait
	 * overrides
	 * 
	 * @param taskList
	 *            tasks list with category weight and no weight override for
	 *            task
	 * @return grade in 0-100 format for the all the task sent to it.
	 */
	private static double taskListGradewithCategoryWeight(final List<Task> taskList) {
		double recieveGrade = 0;
		double maxGrade = 0;
		for (final Task task : taskList) {
			recieveGrade += task.getReceivedGrade()
					.doubleValue();
			maxGrade += task.getMaxGrade()
					.doubleValue();
			
			// Log.d("task.getReceivedGrade() ------"
			// + "					.doubleValue()",
			// ""
			// + task.getReceivedGrade()
			// .doubleValue());
			// Log.d("task.getMaxGrade() ------"
			// + "					.doubleValue()",
			// ""
			// + task.getMaxGrade()
			// .doubleValue());
			
		}
		return ((recieveGrade / maxGrade) * 100);
	}
	
	/**
	 * calculated weight for a single task.
	 * 
	 * @param task
	 *            task needed to calculated weight.
	 * @return the overall weight for the class.
	 * @throws MissingInfoException
	 *             needed info for calculation missing.
	 */
	private static double taskOverallWeightWithNoOverride(final Task task) throws MissingInfoException {
		double categoryWeight;
		double taskWeight;
		double maxCategoryPoints = 0;
		Category category;
		category = task.getCategory();
		
		if (category.getWeight() == null) {
			throw new MissingInfoException("The task has no weight."); //$NON-NLS-1$
		}
		categoryWeight = GradeCal.calculateCategoryWeight(category);
		
		for (final Task catTask : category.getTasks()) {
			if ((catTask.getMaxGrade() == null)
					|| (catTask.getWeightOverride() != null)) {
				continue;
			}
			maxCategoryPoints += catTask.getMaxGrade()
					.doubleValue();
		}
		
		if (task.getMaxGrade() == null) {
			throw new MissingInfoException("The task has no max grade. Please entered max grade for task"); //$NON-NLS-1$
		}
		
		if (maxCategoryPoints == 0) {
			Log.wtf(GradeCal.TAG,
					new ArithmeticException("Should be impossible to run! If you are seeing this something is REALLY wrong")); //$NON-NLS-1$
			throw new ArithmeticException("Should be impossible to run! \nIf you are seeing this something is REALLY wrong"); //$NON-NLS-1$
		}
		
		taskWeight = (task.getMaxGrade()
				.doubleValue() / maxCategoryPoints)
				* categoryWeight;
		return taskWeight;
		
	}
	
	/**
	 * Returns all the points earned for final grade.
	 * 
	 * @param course
	 *            course required for grading
	 * @return total points towards overall graded earned
	 * @throws CategoryException
	 *             task override weight total exceeds category weight
	 */
	private static double totalPointsEarned(final Course course) throws CategoryException {
		double pointsEarned = 0;
		for (final Category category : course.getCategories()) {
			if (category.getWeight() == null) {
				pointsEarned += GradeCal.finalGradeWithoutCategoryWeight(category);
			} else {
				pointsEarned += GradeCal.finalGradeCategoryWeight(category,
						category.getWeight()
								.doubleValue() / 100);
			}
			
		}
		
		return pointsEarned;
	}
	
	/**
	 * This will never be publicly used.
	 */
	private GradeCal() {
		// XXX Write now this is empty for a just in case it is needed.
		
	}
	
}
